Chomu's Blog.

>

Posts

GitHub

Deno 와 Hono 로 CSR 하기

목차

개요

요즘 Deno 가 마음에 들어서 자주 사용하고 있는데, OSSCA 를 시작하며 Hono 라는 웹 프레임워크를 알게 되며 둘을 같이 써보고 싶어졌다. Hono 가 JSX 도 지원하길래 간단하게 사용하면 될 줄 알았는데 CSR 설정이 생각보다 귀찮았다. ~~보통은 반대던데~~ 아무래도 Hono 가 원래 백엔드 프레임워크라 번들링에 대한 설정이 없어서 그런 것 같다. 찾아보니 Vite 나 Webpack 같은 번들러를 사용한다고 해서 Vite까지 써야하나... 하고 고민하던 중에 마침 며칠 전에 나온 Deno 2.4 에 deno bundle 명령어가 있다길래 공부할 겸 사용해봤다. SSR 쓸거면 그냥 파일 확장자만 .tsx로 바꾸고 JSX 로 작성하면 되니 ~~그마저도 귀찮으면 그냥 html 문자열로~~ 굳이 설정할 필요가 없으므로 이 글을 읽을 필요가 없다.

앱 생성

deno init
deno add jsr:@hono/hono

혹은 Hono 공식 문서에 있는

deno init --npm hono .

명령어를 사용해도 된다. 다만 나는 위의 방법을 기준으로 설명하겠다.

deno.json

deno.json 파일이 생성되면 다음과 같이 compilerOptions 옵션을 추가/수정한다.

{
  ...,
  "compilerOptions": {
    "lib": ["esnext", "dom", "deno.ns"],
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

deno init --npm hono . 명령어를 사용하면 "jsx": "precompile", "jsxImportSource": "hono/jsx"로 설정이 돼서 번들링 시에 오류가 발생한다. 그래서 공식 문서를 뒤져서 나온 "jsx": "react-jsx", "jsxImportSource": "hono/jsx/dom" 으로 설정했더니 이번엔 Deno 언어 서버가 hono/jsx에 있는 JSX.IntrinsicElements 를 못 찾아서 컴포넌트마다 오류가 발생했다. 한참 검색도 해보고 LLM 도 열일 시킨 후에 결국 둘을 합친 "jsx": "react-jsx", "jsxImportSource": "hono/jsx" 로 설정하니 오류가 사라졌다.

app.tsx

app.tsx 파일을 적당히 생성하자.

import { render } from "hono/jsx/dom";
 
function App() {
  return (
    <main>
      <h1>Hello, Hono?!</h1>
      <p onClick={() => alert("Welcome to Hono!")}>
        This is a simple Hono application.
      </p>
    </main>
  );
}
 
const body = document!.getElementsByTagName("body")[0];
 
if (!body) {
  throw new Error("Root element not found");
}
 
render(<App />, body);

번들링

Deno는 2.4 버전부터 deno bundle 명령어를 지원한다.

deno bundle [files...]
deno bundle app.tsx

-o 옵션

이렇게만 적으면 files에 있는 파일들을 번들링한 결과를 출력해버린다. > <output> 으로 해도 되지만 -o, --output <output> 옵션으로 파일로 저장할 수 있다.

deno bundle app.tsx -o <output>

--platform 옵션

그리고 --platform <platform> 옵션으로 플랫폼을 지정할 수 있다. 현재는 browser, deno 두가지가 있으며 기본값은 deno 이다. 나는 웹 서버에 사용할 것이므로 --platform browser 옵션을 사용한다.

deno bundle app.tsx --platform browser -o <output>

--minify 옵션

--minify 옵션을 사용하면 번들링된 결과를 최소화할 수 있다.

deno bundle app.tsx --platform browser --minify -o <output>

번들링 결과 예시

작업 경로에 적당한 index.html 파일을 만들고, <script> 태그에 src 속성으로 아까 번들링된 파일<output> 경로를 지정해주자.

<!DOCTYPE html>
<html>
  <head>
    </head>
  <body>
    <main id="main"></main>
    <script type="module" src="<output>"></script>
  </body>
</html>

이 파일을 jsr:@std/http/file-server를 사용하여 정적 파일 서버로 제공할 수 있다.

deno run -ENR jsr:@std/http/file-server

Hono 와 함께 사용하기

이제 Hono를 사용하여 간단한 웹 서버를 만들어보자. main.ts 파일에 다음과 같이 작성한다. <output> 부분은 아까 번들링한 파일의 경로로 바꿔준다.

import { Hono } from 'jsr:@hono/hono';
 
const app = new Hono();
 
 
app.use("/<output>", serveStatic({ root: "./" }));
 
app.get("*", async (c) => {
  try {
    const html = await Deno.readTextFile("./index.html");
    return c.html(html);
  } catch (error) {
    console.error("Error reading index.html:", error);
    return c.text("Error loading page", 500);
  }
});
 
export default app;

번들링 파일은 정적 파일로 제공하고, index.html 파일을 읽어와서 클라이언트에게 응답한다는 내용이다. 나같은 경우는 번들링을 out/bundle.js로 저장한 뒤 아예 /out/* 경로를 정적 파일로 제공하도록 했다.

app.use("/out/*", serveStatic({ root: "./" }));

이제 main.ts 파일을 실행하면 Hono 서버가 시작된다.

deno run -A main.ts

-A 옵션은 모든 권한을 허용하는 옵션이라 보안상 주의가 필요하니 꼭 확인 후 사용하자.

deno task

이제 파일을 수정할 때마다 다음 명령어를 입력해주면 된다.

deno bundle --minify --platform=browser app.tsx -o out/bundle.js
deno run -A main.ts

당연히 귀찮다. 자동으로 번들링하고 서버를 재시작하는 deno task를 만들어보자.

deno.json 파일에 다음과 같이 tasks 항목을 추가한다.

{
  ...,
  "tasks": {
    "dev": "deno bundle --minify --platform=browser app.tsx -o out/bundle.js && deno run -A main.ts"
  }
}

그리고 다음 명령어로 실행한다.

deno task dev

--watch

문제는 이렇게 하면 파일을 수정할 때마다 서버를 껐다가 다시 켜야 한다는 점이다. 두 커멘드에 각각 --watch 옵션을 추가하면 파일이 수정될 때마다 자동으로 번들링하고 서버를 재시작한다.

{
  ...,
  "tasks": {
    "dev": "deno bundle --minify --platform=browser app.tsx -o out/bundle.js --watch && deno run -A main.ts --watch"
  }
}

이 때 deno bundle ... --watch 옵션을 사용하면 파일이 수정되는지 확인하기 위해 deno bundle 명령어가 계속 실행되고 뒤의 deno run ... 명령어는 실행되지 않는다. 그래서 중간에 &&& 로 바꿔줘야한다.

{
  ...,
  "tasks": {
    "dev": "deno bundle --minify --platform=browser app.tsx -o out/bundle.js --watch & deno run -A main.ts --watch"
  }
}

&& 는 이전 명령어가 성공적으로 실행된 후에 다음 명령어를 실행하는 반면, & 는 이전 명령어와 상관없이 다음 명령어를 실행하므로 차이를 잘 알아뒀다가 요긴하게 써먹자. 참고로 <command> & 는 커멘드를 백그라운드에서 실행시켜놓고 다른 커멘드를 실행시킬 수 있어 필요한 경우 종종 사용하기도 한다.

이제 deno task dev 명령어를 실행하면 파일이 수정될 때마다 자동으로 번들링하고 서버를 재시작한다.